Graphics buffer总结

0x1 GPU系统Buffer介绍

本文对GPU系统中的buffer管理进行了总结。
简单说来GPU系统使用到的buffer主要包括两部分,其中一部分是GPU绘制输出的frame buffer, 另外一部分是GPU driver中内部运行所需的各种buffer。
这些buffer在CPU和GPU之间的pipeline如下图所示。
从图中可知,Graphics程序运行的时候首先需要CPU先准备好各种数据,如纹理,顶点数据等。另外很大一块是Command,这个是用来驱动GPU硬件执行图形流水线的指令。另外Shader指的是用来执行GPU可编程pileine(如vertex shader,fragment shader等)的GPU指令。
CPU把数据准备好了以后,驱动GPU硬件根据前面buffer里的数据和指令执行绘制工作,绘制的结果需要输出到一个地方,这个就是frame buffer。当然我们知道frame buffer也是可以被GPU再读进来。图中的Bin List部分指的是在Tile Based Render架构特有的buffer,这个buffer用来存储Tile划分(一般称为Binning阶段)以后每个Tile需要绘制的信息。

0x2 Frame Buffer

下面先来介绍一下Android系统中Graphics Buffer管理模块,后续再介绍一下DRM结构下Buffer分配的流程。
Android系统中Graphics Buffer的架构图如下所示。

Android系统中Graphics框架或应用通过Gralloc模块调用到ION driver,再通过ION driver分配出相应的frame buffer。如上图所示,ION driver中提供多种不同的heap。Carveout heap一般是指系统启动的时候预分配好的物理连续地址空间,缺点是这部分内存属于特定驱动独占式的,不能和其他模块共享,目前已经很少使用了。CMA(contiguous memory allocation) heap分配出来的buffer的物理地址也是连续的,但是它是在系统运行过程中通过内核中CMA框架来动态分配的。system heap分配出来的buffer其物理地址一般不是连续的。这三种类型的buffer适用于不同的硬件类型。对没有MMU的硬件来说,一般需要要求其访问的物理地址空间是连续的,所以这种硬件一般要求分配的buffer是CMA heap。对包含MMU的硬件来说,由于可以通过MMU(也称为IOMMU)来做虚拟地址到物理地址的转换,所以可以不需要保证其分配的buffer的物理地址空间是连续的。

下面来介绍一下DRM架构下buffer分配的流程。
传统的Linux系统如Ubuntu系统采用的是DRM架构图形系统,这个时候通过DRM kernel driver中的GEM模块来分配buffer。GEM是DRM kernel driver的buffer管理模块。其分配出来的buffer一般称为GEM buffer。其buffer分配过程简单说明如下。

为了简化DRM架构下GEM buffer的分配工作,其提供了libgbm模块作为管理kernel driver和图形应用之间的buffer分配的桥梁。
libgbm的功能类似于Android上的gralloc+libion。mesa中包括了libgbm的实现代码。libgbm提供了底层DRM driver中buffer管理的封装。一般包括分配(DRM_IOCTL_MODE_CREATE_DUMB),释放(DRM_IOCTL_MODE_DESTROY_DUMB)和mmap操作(DRM_IOCTL_MODE_MAP_DUMB)等操作。各种采用Linux作为内核的操作系统的HAL模块都提供了类似libgbm功能的buffer管理模块。

下面来简单介绍一下为什么要执行 DRM_IOCTL_MODE_MAP_DUMB? DRM_IOCTL_MODE_MAP_DUMB的输入是一个 gem handle,返回结果是一个 offset,通过 offset 可以知道 mmap 当前要操作的dumb buffer。所以对 drm device 进行 mmap 操作时,其参数offset 并不是真正的内存偏移量,而是一个 gem object 的索引值。通过该索引值,drm 驱动就可以准确确定当前要操作的是哪个 gem对象,然后可以获取到与该 object 相对应的物理 buffer,并对完成真正的 mmap 操作。

libgbm中DRM_IOCTL_MODE_MAP_DUMB的使用如下所示。可以看到和前面的解析是可以对应起来的。

1
2
3
4
5
6
7
8
map_arg.handle = bo->handle;
ret = drmIoctl(bo->base.gbm->fd, DRM_IOCTL_MODE_MAP_DUMB, &map_arg);
if (ret)
return NULL;
bo->map = mmap(0, bo->size, PROT_WRITE,
MAP_SHARED, bo->base.gbm->fd, map_arg.offset);

下面来说一下mesa driver中frame buffer分配过程。
调用eglCreateWindowSurface之类的函数会指明需要创建Suface。这个时候一般不会真正分配buffer,而是会创建类似buffer的占位符的Suface对象。然后等真正需要使用这个buffer的时候才完成分配工作,这里面体现了defer分配的思想。
下面介绍一下真正需要分配buffer的时候buffer是如何分配出来的。
首先通过eglMakeCurrent分配,具体流程如下。这个分配出来的buffer供给后续的eglSwapBuffer使用。

eglSwapBuffer在前面分配好的buffer执行完绘制工作以后,对应的buffer可能送去合成模块继续执行合成动作了。如果后面再调用glClear,需要分配新的buffer,具体流程如下。

在eglMakeCurrent和glClear调用执行buffer分配工作的时候,需要找到一块目标buffer执行clear操作,如果这个时候没有可用的buffer,或者buffer大小不符合要求,则需要重新分配一块新的buffer。

前面看到,不同的Graphics API函数都可能触发buffer的分配,但是每个egl context可以分配buffer的数量是有限制的,一般是2~3个。如果buffer用完了,这个时候可能就需要等待一个空的可用的buffer从其他的pipeline中释放出来。如我们可能在systrace上看到glClear函数占用很长时间,这个时候就会感觉很奇怪,因为glClear按理说不应该占用GPU太长时间,这个时候大概率是在等待一个空的buffer变成可用,并不是在GPU在执行繁重的绘制任务。

0x3 GPU driver中内部使用的buffer

这里介绍的buffer一般是指GPU driver运行过程中内部需要的各种buffer,如常见的texture buffer,vertex buffer,shader buffer, command buffer。这种buffer一般用来保存GPU需要读取的各种数据, 这种buffer的使用一般流程是先通过kernel driver分配出来,然后mmap到CPU端,CPU完成写入以后,GPU开始读取。GPU读取的时候会涉及到GPU MMU的动作,需要完成GPU虚拟地址到GPU物理地址的转换。

Andorid的GPU驱动架构下,其内部使用的buffer管理一般是char kernel driver+闭源User space library的架构来实现。kernel driver通过alloc_pages之类的底层API直接分配buffer,然后mmap到User space给CPU写入,写入完成通过ioctl通知GPU硬件开始使用这些buffer。

下面来简单介绍一下Intel Gen i915 kernel driver中GEM buffer实现。
i915 kernel driver提供了下面三种gem buffer实现。
其中shmem_region_ops采用的是shmem机制来分配内存,shmem是一套ipc,通过相应的ipc系统调用shmget能够以指定key创建一块的共享内存。需要使用这块内存的进程可以通过shmat系统调用来获得它。
stolen buffer指的通过GTT(Graphics translation table)来管理的buffer,这里面类似GTT,PPGTT的概念都是很大的一块技术,这里就不深入介绍了。
下面直接贴出i915 kernel driver中三种gem buffer定义和初始化的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
enum intel_region_id {
INTEL_REGION_SMEM = 0,
INTEL_REGION_LMEM,
INTEL_REGION_STOLEN,
INTEL_REGION_UNKNOWN, /* Should be last */
};
static const struct intel_memory_region_ops shmem_region_ops = {
.init = init_shmem,
.release = release_shmem,
.create_object = create_shmem,
};
static const struct intel_memory_region_ops i915_region_stolen_ops = {
.init = init_stolen,
.release = release_stolen,
.create_object = _i915_gem_object_create_stolen,
};
const struct intel_memory_region_ops intel_region_lmem_ops = {
.init = region_lmem_init,
.release = region_lmem_release,
.create_object = __i915_gem_lmem_object_create,
};
int intel_memory_regions_hw_probe(struct drm_i915_private *i915)
{
int err, i;
for (i = 0; i < ARRAY_SIZE(i915->mm.regions); i++) {
struct intel_memory_region *mem = ERR_PTR(-ENODEV);
u32 type;
if (!HAS_REGION(i915, BIT(i)))
continue;
type = MEMORY_TYPE_FROM_REGION(intel_region_map[i]);
switch (type) {
case INTEL_MEMORY_SYSTEM:
mem = i915_gem_shmem_setup(i915);
break;
case INTEL_MEMORY_STOLEN:
mem = i915_gem_stolen_setup(i915);
break;
case INTEL_MEMORY_LOCAL:
mem = intel_setup_fake_lmem(i915);
break;
}
if (IS_ERR(mem)) {
err = PTR_ERR(mem);
drm_err(&i915->drm,
"Failed to setup region(%d) type=%d\n",
err, type);
goto out_cleanup;
}
mem->id = intel_region_map[i];
mem->type = type;
mem->instance = MEMORY_INSTANCE_FROM_REGION(intel_region_map[i]);
i915->mm.regions[i] = mem;
}
......
}

另外我们知道buffer的分配是很heavy的操作,gpu驱动一般都提供了cache机制来缓存使用过的buffer,也就是说延迟释放这些使用过的buffer。mesa和broadcom gpu驱动中实现了用户态驱动的cache机制,mali早期gpu(mali400/450)在kernel space driver中加了cache机制。从性能的角度的来说,用户态驱动的cache应该更好一些,毕竟少了从用户态到内核态的调用,但是用户态驱动的cache实现复杂一些。